package com.eralink.readerfinderlib;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

class ReaderFinder {
    private EraLinkReaderReportCallback reportCallback;

    private final Semaphore semaphore = new Semaphore(0);
    private final AtomicInteger waitCmd = new AtomicInteger(0);

    private static final Hashtable<Integer, Processor> processors = new Hashtable<>();
    private final Hashtable<Integer, CacheNode> responseCache = new Hashtable<>();

    private final LinkedBlockingDeque<CacheNode> reportQueue = new LinkedBlockingDeque<>();

    DatagramSocket socket;
    private volatile boolean threadRunning = false;
    private Thread receiveThread = null;
    private Thread dispatchThread = null;

    private ReaderFinder() {
        Processor processor = new Processor(ReaderFinderCmdCode.CmdDiscovery.getValue(), "discovery reader");
        processor.setRequestor(this::discoveryRequester);
        processor.setNeedNotifyRequestor(false);
        processor.setResponseValidator(this::discoveryReportValidator);
        processor.setReportUtilizer(this::discoveryReportUtilizer);
        processors.put(ReaderFinderCmdCode.CmdDiscovery.getValue(), processor);

        processor = new Processor(ReaderFinderCmdCode.CmdGetNetConfig.getValue(), "get reader net config");
        processors.put(ReaderFinderCmdCode.CmdGetNetConfig.getValue(), processor);

        processor = new Processor(ReaderFinderCmdCode.CmdSetNetConfig.getValue(), "set reader net config");
        processors.put(ReaderFinderCmdCode.CmdSetNetConfig.getValue(), processor);

        processor = new Processor(ReaderFinderCmdCode.CmdRestart.getValue(), "restart reader");
        processor.setNeedNotifyRequestor(false);
        processors.put(ReaderFinderCmdCode.CmdRestart.getValue(), processor);
    }

    private static class SingletonClassInstance {
        private static final ReaderFinder instance = new ReaderFinder();
    }

    public static ReaderFinder getInstance() {
        return SingletonClassInstance.instance;
    }

    public int open(short listen_port) {
        if (threadRunning) {
            return EraLinkReaderFinderApiError.Err_yet_opened.getValue();
        }

        try {
            socket = new DatagramSocket(listen_port);
            socket.setSoTimeout(20);
        } catch (SocketException e) {
            e.printStackTrace();
            if (socket != null) {
                socket.close();
                socket = null;
            }
            return EraLinkReaderFinderApiError.Err_open_failed.getValue();
        }

        startThreaad();

        return 0;
    }

    public int close() {
        stopThread();
        if (socket != null) {
            socket.close();
            socket = null;
        }

        return 0;
    }

    public void setReaderReportCallback(EraLinkReaderReportCallback reportCallback) {
        this.reportCallback = reportCallback;
    }

    public int discovery() {
        return doRequest(ReaderFinderCmdCode.CmdDiscovery.getValue());
    }

    private int discoveryRequester() {
        final byte[] request = {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
                0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, (byte) 0xC0, 0x01, 0x01, 0x00, 0x01,
                0x00, 0x01, 0x00, 0x01};
        return writeRequest(request, 0, request.length);
    }

    private final byte[] ResponseFirst = new byte[]{0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
    private final byte[] ResponseSecond = new byte[]{0x01, 0x00, (byte)0xC0, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01};
    private int discoveryReportValidator(CacheNode cacheNode) {
        if (!Utils.bytesEqual(cacheNode.getBuffer(), 4, 10, ResponseFirst, 0, ResponseFirst.length) ||
            !Utils.bytesEqual(cacheNode.getBuffer(), 20, 9, ResponseSecond, 0, ResponseSecond.length)) {
            return EraLinkReaderFinderApiError.Err_bad_report.getValue();
        }

        return EraLinkReaderFinderApiError.Err_no_error.getValue();
    }

    private void discoveryReportUtilizer(CacheNode cacheNode) {
        final byte[] report = cacheNode.getBuffer();
        final int report_len = cacheNode.getBufferSize();
        StringBuilder builder = new StringBuilder();
        final String ipAddress = Utils.ipV4AddrFromU32(Utils.u32FromBigEndianU8s(report, 0));
        builder.append(String.format("ip=%s", ipAddress));
        builder.append(String.format(";mac_address=%s", Utils.bytesToHexString(report, 14, 6)));
        while (true) {
            if (34 > report_len) {
                break;
            }

            if (0x03 != report[31]) { // 版本号
                break;
            }
            final int version_len = report[32] & 0xFF;
            if (0 == version_len || (report_len - 33) < version_len) {
                break;
            }
            builder.append(String.format(";version=%s", new String(report, 33, version_len)));

            if (37 > report_len) {
                break;
            }

            if (0x02 != report[33 + version_len]) { // 设备名称
                break;
            }
            final int name_len = report[34 + version_len] & 0xFF;
            if (0 == name_len || (report_len - 35 - version_len) < name_len) {
                break;
            }
            builder.append(String.format(";name=%s", new String(report, 35 + version_len, name_len)));

            if (7 > (report_len - 35 - version_len - name_len)) {
                break;
            }

            if (0x51 != report[35 + version_len + name_len] || 0x02 != report[36 + version_len + name_len] ||
                    0x50 != report[39 + version_len + name_len] || 0x01 != report[40 + version_len + name_len]) {
                break;
            }
            if (1 != report[41 + version_len + name_len] && 0 != report[41 + version_len + name_len]) {
                break;
            }
            builder.append(String.format(";net_mode=%s", report[41 + version_len + name_len] & 0xFF));

            break;
        }

        final byte[] data = builder.toString().getBytes();
        if (reportCallback != null) {
            reportCallback.report(data, data.length);
        }
    }

    private static final byte[] RequestHead = {
            0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x01, 0x00, (byte)0xC0, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00
    };
    public int getNetConfig(String mac_address, final String[] net_config) {
        Processor processor = processors.get(ReaderFinderCmdCode.CmdGetNetConfig.getValue());
        processor.setRequestValidator(() ->
                mac_address != null && mac_address.length() == 12 &&
                mac_address.matches("^[A-Fa-f0-9]+$") && net_config != null && 0 < net_config.length);
        final byte[][] macAddress = {null};
        processor.setRequestor(() -> {
            final byte[] request = Arrays.copyOf(RequestHead, RequestHead.length);
            request[request.length - 1] = 0x02;
            macAddress[0] = Utils.hexStrToBytes(mac_address);
            System.arraycopy(macAddress[0], 0, request, 2, macAddress[0].length);
            return writeRequest(request, 0, request.length);
        });
        final String[] response_data = new String[1];
        processor.setResponseValidator(cacheNode -> {
            final byte[] response = cacheNode.getBuffer();
            final int response_len = cacheNode.getBufferSize();
            if (!Utils.bytesEqual(response, 4, 10, ResponseFirst, 0, ResponseFirst.length) ||
                !Utils.bytesEqual(response, 14, 6, macAddress[0], 0, macAddress[0].length) ||
                !Utils.bytesEqual(response, 20, 9, ResponseSecond, 0, ResponseSecond.length)) {
                return EraLinkReaderFinderApiError.Err_bad_response.getValue();
            }

            StringBuilder builder = new StringBuilder();
            boolean bad_response = true;
            while (true){
                if (77 > response_len) {
                    break;
                }
                if (0x04 != response[31] || 0x01 != response[32] || (0x00 != response[33] && 0x01 != response[33])) { // DHCP
                    break;
                }
                builder.append(String.format("dhcp=%s", response[33] & 0xFF));

                if (0x07 != response[34] || 0x04 != response[35]) { // 网关
                    break;
                }
                builder.append(String.format(";gateway=%s", Utils.ipV4AddrFromU32(Utils.u32FromBigEndianU8s(response, 36))));

                if (0x06 != response[40] || 0x04 != response[41]) { // 掩码
                    break;
                }
                builder.append(String.format(";mask=%s", Utils.ipV4AddrFromU32(Utils.u32FromBigEndianU8s(response, 42))));

                if (0x05 != response[46] || 0x04 != response[47]) { // IP
                    break;
                }
                builder.append(String.format(";ip=%s", Utils.ipV4AddrFromU32(Utils.u32FromBigEndianU8s(response, 48))));

                if (0x41 != response[52] || 0x02 != response[53]) { // port
                    break;
                }
                builder.append(String.format(";port=%s", Utils.u16FromBigEndianU8s(response, 54)));

                if (0x43 != response[62] || 0x04 != response[63]) { // baud
                    break;
                }
                builder.append(String.format(";baud=%s", Utils.u32FromBigEndianU8s(response, 64)));

                if (0x44 != response[68] && 0x01 != response[69]) { // parity
                    break;
                }
                builder.append(String.format(";parity=%s", response[70] & 0xFF));

                if (0x45 != response[71] || 0x01 != response[72]) { // databits
                    break;
                }
                builder.append(String.format(";databits=%s", response[73] & 0xFF));

                if (0x46 != response[74] || 0x01 != response[75]) { // stopbits
                    break;
                }
                builder.append(String.format(";stopbits=%s", response[76] & 0xFF));

                bad_response = false;

                if (87 > response_len) {
                    break;
                }

                bad_response = true;

                if (0x48 != response[77] || 0x04 != response[78]) {
                    break;
                }
                builder.append(String.format(";server_ip=%s", Utils.ipV4AddrFromU32(Utils.u32FromBigEndianU8s(response, 79))));

                if (0x49 != response[83] || 0x02 != response[84]) {
                    break;
                }
                builder.append(String.format(";server_port=%s", Utils.u16FromBigEndianU8s(response, 85)));

                if (94 > response_len) {
                    break;
                }

                if (0x50 != response[87] || 0x01 != response[88] || (0 != response[89] && 1 != response[89])) {
                    break;
                }
                builder.append(String.format(";net_mode=%s", response[89] & 0xFF));

                if (0x51 != response[90] || 0x02 != response[91]) {
                    break;
                }
                builder.append(String.format(";product_code=%s", Utils.u16FromBigEndianU8s(response, 92)));

                bad_response = false;
                break;
            }

            if (bad_response) {
                return EraLinkReaderFinderApiError.Err_bad_response.getValue();
            }

            response_data[0] = builder.toString();

            return EraLinkReaderFinderApiError.Err_no_error.getValue();
        });
        processor.setResponseUtilizer(response -> net_config[0] = response_data[0]);

        return doRequest(ReaderFinderCmdCode.CmdGetNetConfig.getValue());
    }

    public int setNetConfig(String mac_address, final String net_config) {
        final NetConfigParser configParser = new NetConfigParser();
        Processor processor = processors.get(ReaderFinderCmdCode.CmdSetNetConfig.getValue());
        final byte[][] macAddress = {null};
        processor.setRequestValidator(() -> {
            if (mac_address == null || mac_address.length() != 12 || !mac_address.matches("^[A-Fa-f0-9]+$")) {
                return false;
            }

            if (!configParser.parse(net_config) || !configParser.validate()) {
                return false;
            }

            return true;
        });
        processor.setRequestor(() -> {
            final byte[] request = Arrays.copyOf(RequestHead, 62);
            request[RequestHead.length - 1] = 0x03;
            macAddress[0] = Utils.hexStrToBytes(mac_address);
            System.arraycopy(macAddress[0], 0, request, 2, macAddress[0].length);

            final String[] value = new String[1];
            configParser.getValueByName("ip", value);
            Utils.u32ToBigEndianU8s(Utils.ipV4AddrToU32(value[0]), request, 27);

            configParser.getValueByName("mask", value);
            Utils.u32ToBigEndianU8s(Utils.ipV4AddrToU32(value[0]), request, 31);

            configParser.getValueByName("gateway", value);
            Utils.u32ToBigEndianU8s(Utils.ipV4AddrToU32(value[0]), request, 35);

            configParser.getValueByName("dhcp", value);
            request[39] = (byte) Integer.parseInt(value[0]);

            configParser.getValueByName("port", value);
            Utils.u16ToBigEndianU8s(Short.parseShort(value[0]), request, 40);

            System.arraycopy(new byte[]{0x01, 0x0D, 0x00, 0x0A}, 0, request, 42, 4);

            configParser.getValueByName("baud", value);
            Utils.u32ToBigEndianU8s(Integer.parseInt(value[0]), request, 46);

            configParser.getValueByName("parity", value);
            request[50] = Byte.parseByte(value[0]);

            configParser.getValueByName("databits", value);
            request[51] = Byte.parseByte(value[0]);

            configParser.getValueByName("stopbits", value);
            request[52] = Byte.parseByte(value[0]);
            int request_len = 53;
            if (configParser.getValueByName("server_ip", value)) {
                Utils.u32ToBigEndianU8s(Utils.ipV4AddrToU32(value[0]), request, 53);

                configParser.getValueByName("server_port", value);
                Utils.u16ToBigEndianU8s(Short.parseShort(value[0]), request, 57);

                configParser.getValueByName("net_mode", value);
                request[59] = Byte.parseByte(value[0]);

                configParser.getValueByName("product_code", value);
                Utils.u16ToBigEndianU8s(Short.parseShort(value[0]), request, 60);

                request_len = 62;
            }

            return writeRequest(request, 0, request_len);
        });
        processor.setResponseValidator(cacheNode -> {
            final byte[] response = cacheNode.getBuffer();
            if (!Utils.bytesEqual(response, 4, 10, ResponseFirst, 0, ResponseFirst.length) ||
                    !Utils.bytesEqual(response, 14, 6, macAddress[0], 0, macAddress[0].length) ||
                    !Utils.bytesEqual(response, 20, 9, ResponseSecond, 0, ResponseSecond.length)) {
                return EraLinkReaderFinderApiError.Err_bad_response.getValue();
            }

            return EraLinkReaderFinderApiError.Err_no_error.getValue();
        });

        return doRequest(ReaderFinderCmdCode.CmdSetNetConfig.getValue());
    }

    public int restart(String mac_address) {
        Processor processor = processors.get(ReaderFinderCmdCode.CmdRestart.getValue());
        processor.setRequestValidator(() ->
                mac_address != null && mac_address.length() == 12 && mac_address.matches("^[A-Fa-f0-9]+$"));
        processor.setRequestor(() -> {
            final byte[] request = Arrays.copyOf(RequestHead, RequestHead.length);
            request[request.length - 1] = 0x04;
            final byte[] macAddress = Utils.hexStrToBytes(mac_address);
            System.arraycopy(macAddress, 0, request, 2, macAddress.length);
            return writeRequest(request, 0, request.length);
        });

        return doRequest(ReaderFinderCmdCode.CmdRestart.getValue());
    }

    private int doRequest(int cmdCode) {
        Processor processor = processors.get(cmdCode);
        if (null == processor) {
            return EraLinkReaderFinderApiError.Err_request_failed.getValue();
        }

        Processor.RequestValidator requestValidator = processor.getRequestValidator();
        if (requestValidator != null) {
            if (!requestValidator.validate()) {
                return EraLinkReaderFinderApiError.Err_bad_arg.getValue();
            }
        }

        if (processor.isNeedNotifyRequestor()) {
            if (!waitCmd.compareAndSet(0, cmdCode)) {
                return EraLinkReaderFinderApiError.Err_need_retry.getValue();
            }
        }

        int ret;

        CacheNode response = null;
        do {
            ret = processor.getRequestor().request();
            if (EraLinkReaderFinderApiError.Err_no_error.getValue() != ret) {
                break;
            }

            if (processor.isNeedNotifyRequestor()) {
                try {
                    if (!semaphore.tryAcquire(3, TimeUnit.SECONDS)) {
                        ret = EraLinkReaderFinderApiError.Err_timeout.getValue();
                        break;
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    ret = EraLinkReaderFinderApiError.Err_interrupted.getValue();
                    break;
                }

                response = responseCache.remove(cmdCode);
                if (response == null) {
                    ret = EraLinkReaderFinderApiError.Err_wait_no_response.getValue();
                    break;
                }

                ret = processor.getResponseValidator().validate(response);
                if (EraLinkReaderFinderApiError.Err_no_error.getValue() != ret) {
                    break;
                }

                Processor.ResponseUtilizer responseUtilizer = processor.getResponseUtilizer();
                if (null != responseUtilizer) {
                    responseUtilizer.utilize(response);
                }
            }
        } while (false);

        if (processor.isNeedNotifyRequestor()) {
            waitCmd.set(0);
        }

        CachePool.getInstance().release(response);

        return ret;
    }

    // 发送数据
    private int writeRequest(final byte[] request, final int offset, final int length) {
        InetSocketAddress address = new InetSocketAddress("255.255.255.255", 17777);
        System.err.printf("request: %s\n", Utils.bytesToHexString(request, 0, length));
        DatagramPacket packet = new DatagramPacket(request, offset, length, address);
        try {
            socket.send(packet);
        } catch (IOException e) {
            e.printStackTrace();
            return EraLinkReaderFinderApiError.Err_io_failed.getValue();
        }

        return 0;
    }

    private void startThreaad() {
        threadRunning = true;
        receiveThread = new Thread(() -> {
            final byte[] buffer = new byte[1024];
            DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
            while (threadRunning) {
                packet.setData(buffer, 0, buffer.length);
                try {
                    socket.receive(packet);
                } catch (SocketTimeoutException e) {
                    continue;
                }
                catch (IOException e) {
                    e.printStackTrace();
                    threadRunning = false;
                    break;
                }

                System.err.printf("ip: %s, data: %s\n", packet.getAddress().getHostAddress(),
                        Utils.bytesToHexString(packet.getData(), 0, packet.getLength()));

                if (27 > packet.getLength()) {
                    continue;
                }

                final byte[] address = packet.getAddress().getAddress();
                CacheNode report = CachePool.getInstance().obtain(address.length + packet.getLength());
                System.arraycopy(address, 0, report.getBuffer(), 0, address.length);
                System.arraycopy(packet.getData(), 0, report.getBuffer(), address.length, packet.getLength());
                report.setBufferSize(address.length + packet.getLength());
                ReaderFinder.this.reportQueue.add(report);
            }
        });
        receiveThread.start();

        dispatchThread = new Thread(() -> {
            while (threadRunning) {
                CacheNode cacheNode;
                try {
                    cacheNode = reportQueue.poll(20, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }

                if (cacheNode == null) {
                    continue;
                }

                System.out.printf("reader report: %s\n", Utils.bytesToHexString(cacheNode.getBuffer(), 0, cacheNode.getBufferSize()));

                final int cmd_code = Utils.u16FromBigEndianU8s(cacheNode.getBuffer(), 29);
                Processor processor = processors.get(cmd_code);
                if (processor == null) {
                    System.err.printf("unknown cmd: %d\n", cmd_code);
                    CachePool.getInstance().release(cacheNode);
                    continue;
                }

                if (processor.isNeedNotifyRequestor()) {
                    if (waitCmd.compareAndSet(cmd_code, cmd_code)) {
                        responseCache.put(cmd_code, cacheNode);
                        semaphore.release();
                    }
                } else {
                    Processor.ResponseValidator responseValidator = processor.getResponseValidator();
                    if (null != responseValidator) {
                        if (EraLinkReaderFinderApiError.Err_no_error.getValue() != responseValidator.validate(cacheNode)) {
                            CachePool.getInstance().release(cacheNode);
                            continue;
                        }
                    }

                    Processor.ReportUtilizer reportUtilizer = processor.getReportUtilizer();
                    if (reportUtilizer != null) {
                        reportUtilizer.utilize(cacheNode);
                    }

                    CachePool.getInstance().release(cacheNode);
                }
            }
        });
        dispatchThread.start();
    }

    private void stopThread() {
        if (threadRunning) {
            threadRunning = false;
        }
        if (receiveThread != null) {
            try {
                receiveThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            receiveThread = null;
        }
        if (dispatchThread != null) {
            try {
                dispatchThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            dispatchThread = null;
        }
    }
}
